package com.hmdp.utils;

import cn.hutool.core.lang.Tuple;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import com.hmdp.common.Constants;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.lang.reflect.Type;
import java.time.LocalDateTime;
import java.util.concurrent.*;
import java.util.function.Function;

@Slf4j
@RequiredArgsConstructor
@Component
public class RedisCache {
    private final StringRedisTemplate stringRedisTemplate;

    private final RedissonClient redissonClient;

    public <T, R> R safeGet(@NonNull String key, Class<R> clazz, T index, Function<T, R> dbFunction, Long timeout, TimeUnit timeUnit) {
        String jsonStr = stringRedisTemplate.opsForValue().get(key);

        if (StrUtil.isBlank(jsonStr)) {
            // 如果为空值说明数据库不存在
            if (jsonStr != null) {
                return null;
            }

            R data = dbFunction.apply(index);
            if (data == null) {
                RedisData redisData = new RedisData();
                redisData.setData("");
                redisData.setExpireTime(LocalDateTime.now().plusMinutes(Constants.CACHE_NULL_TTL));

                stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
                return null;
            }

            // 这里一开始没有实现，导致如果data不为null会直接执行下面的逻辑，由于jsonStr是空，下面会报错
            RedisData redisData = new RedisData();
            redisData.setData(JSONUtil.toJsonStr(data));
            redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(timeout)));
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
            return data;
        }

        RedisData redisData = JSONUtil.toBean(jsonStr, RedisData.class);
        R data = JSONUtil.toBean(redisData.getData().toString(), clazz);
        LocalDateTime expireTime = redisData.getExpireTime();
        if (expireTime.isAfter(LocalDateTime.now())) {
            return data;
        }

        RLock lock = redissonClient.getLock("cache:lock:" + key);
        // 要判断一下是否获取到了锁
        boolean isLock = lock.tryLock();
        if (isLock) {
            try {
                // 二次验证，因为可能获取锁的时候已经有线程完成了更新的操作
                String tmp = stringRedisTemplate.opsForValue().get(key);
                if (StrUtil.isNotBlank(tmp)) {
                    RedisData redisDataTmp = JSONUtil.toBean(tmp, RedisData.class);
                    if (redisDataTmp.getExpireTime().isAfter(LocalDateTime.now())) {
                        return JSONUtil.toBean(redisDataTmp.getData().toString(), clazz);
                    }
                }
                // 不能创建子线程来更新缓存，因为子线程无法解锁（Redisson依靠线程id为解锁依据）
                R newDbData = dbFunction.apply(index);
                RedisData newData = new RedisData();
                newData.setData(newDbData);
                newData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(timeout)));
                stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(newData));
            } catch (Exception e) {
                // 发生异常要确保锁释放，可能线程池出现异常，导致执行不到finally
                lock.unlock();
                log.error(e.getMessage(), e);
            } finally {
                lock.unlock();
            }
        }
        return data;
    }

    public <T, R> R safeGet(@NonNull String key, TypeReference<R> typeReference, T index, Function<T, R> dbFunction, Long timeout, TimeUnit timeUnit) {
        String jsonStr = stringRedisTemplate.opsForValue().get(key);

        if (StrUtil.isBlank(jsonStr)) {
            // 如果为空值说明数据库不存在
            if (jsonStr != null) {
                return null;
            }

            R data = dbFunction.apply(index);
            if (data == null) {
                RedisData redisData = new RedisData();
                redisData.setData("");
                redisData.setExpireTime(LocalDateTime.now().plusMinutes(Constants.CACHE_NULL_TTL));

                stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
                return null;
            }

            // 这里一开始没有实现，导致如果data不为null会直接执行下面的逻辑，由于jsonStr是空，下面会报错
            RedisData redisData = new RedisData();
            redisData.setData(JSONUtil.toJsonStr(data));
            redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(timeout)));
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
            return data;
        }

        RedisData redisData = JSONUtil.toBean(jsonStr, RedisData.class);
        R data = JSONUtil.toBean(redisData.getData().toString(), typeReference, true);
        LocalDateTime expireTime = redisData.getExpireTime();
        if (expireTime.isAfter(LocalDateTime.now())) {
            return data;
        }

        RLock lock = redissonClient.getLock("cache:lock:" + key);
        // 要判断一下是否获取到了锁
        boolean isLock = lock.tryLock();
        if (isLock) {
            try {
                // 二次验证，因为可能获取锁的时候已经有线程完成了更新的操作
                String tmp = stringRedisTemplate.opsForValue().get(key);
                if (StrUtil.isNotBlank(tmp)) {
                    RedisData redisDataTmp = JSONUtil.toBean(tmp, RedisData.class);
                    if (redisDataTmp.getExpireTime().isAfter(LocalDateTime.now())) {
                        return JSONUtil.toBean(redisData.getData().toString(), typeReference, true);
                    }
                }
                // 不能创建子线程来更新缓存，因为子线程无法解锁（Redisson依靠线程id为解锁依据）
                R newDbData = dbFunction.apply(index);
                RedisData newData = new RedisData();
                newData.setData(newDbData);
                newData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(timeout)));
                stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(newData));
            } catch (Exception e) {
                // 发生异常要确保锁释放，可能线程池出现异常，导致执行不到finally
                lock.unlock();
                log.error(e.getMessage(), e);
            } finally {
                lock.unlock();
            }
        }
        return data;
    }

    public Tuple getTuple(@NonNull String key, Type... types) {
        String jsonStr = stringRedisTemplate.opsForValue().get(key);

        if (StrUtil.isBlank(jsonStr)) {
            return null;
        }

        RedisData redisData = JSONUtil.toBean(jsonStr, RedisData.class);
        Object data = redisData.getData();
        JSONArray jsonArray = JSONUtil.parseArray(data);
        if (jsonArray.size() != types.length) {
            log.error("JSON数组的元素数量{}与期望的类型数量{}不匹配", jsonArray.size(), types.length);
        }
        Object[] members = new Object[types.length];
        for (int i = 0; i < types.length; i++) {
            members[i] = JSONUtil.toBean(jsonArray.get(i).toString(), types[i], true);
            System.out.println(members[i]);
        }
        return new Tuple(members);
    }

    public void set(String key, Object value) {
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.MAX);
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    public void set(String key, Object value, long timeout, TimeUnit timeUnit) {
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(timeout)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }
}
